<?php
/**
 * Mathmulti Plugin: Render (La)Tex or plain text Math expression
 * into images or mathml
 * http://wiki.splitbrain.org/plugin:math
 *
 * Thanks to everyone who provided so many examples of plugin or
 * contributed to the plugin tutorials... This was of great help!
 * If you see part of your code here and I forgot to give credits
 * for your work, please let me know so I could add your name.
 *
 * Syntax:  <math format idiom size>...mathematical formula..</math>
 *
 * format   (optional) Specify which program will be used to
 *           render the math formulae.
 *           Support:
 *           img (use mimetex http://www.forkosh.com/mimetex.html)
 *           mml (use:
 *               itex2mml http://pear.math.pitt.edu/mathzilla/itex2mml.html
 *               plain2mml http://math.wcupa.edu/~johnston/plain2mathml )
 *           If might be better not to specify the format thus you
 *           can change it at once for all math expression in your
 *           wiki by changing the config value.
 * idiom     (optional) Specify in which idiom (math dialect)
 *           the math formulae is written in
 *           So far, only 'tex' [(La)TeX] and 'plain' style are supported
 *           'tex': http://www.forkosh.com/mimetex.html
 *               or http://pear.math.pitt.edu/mathzilla/itex2mml.html
 *           'plain': http://math.wcupa.edu/~johnston/plain2mathml/
 *                    please note that output for plain are in mml only
 *                    if img is specified, the raw text is displayed
 *           Might be used to merge with Christopher Smith's
 *           math plugin (phpmathpublisher own synthax)
 * size     (optional) Default size of the Math char [points]
 *
 * Formulae syntax: (depend on Idiom) See mimetex, itex2mml, plain2mml above
 *                  (may add phpmathpublisher own synthax)
 *
 * Config (in dokuwiki/conf/local.php)
 * $conf['mathmulti_iscgi'] = false; //Are eqn images produced by cgi every time the page is viewed?
 * $conf['mathmulti_mimetex'] = ""; //Path to mimetex bin (produce img)
 * $conf['mathmulti_itex2mml'] = ""; //Path to itex2mml bin (produce mathml)
 * $conf['mathmulti_plain2mml'] = ""; //Path to plain2mml bin (produce mathml)
 * $conf['mathmulti_format'] = "img"; //default mathmulti rendering format
 * $conf['mathmulti_idiom'] = "tex"; //default mathmulti math idiom (dialect)
 * $conf['mathmulti_size'] = "12"; //default mathmulti math char size [points]
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Stephane Chamberland <stephane.chamberland@ec.gc.ca>
 * @date       2006-05-25
 */

if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'syntax.php');
require_once(DOKU_INC.'inc/io.php');

// ----[ mathmulti plugin globals ]---------------------------------------
global $mathmultiplugin_urlimg,$mathmultiplugin_dirimg;

  // base url to access images, should correspond to $dirimg below.
  // if left at default, it will be modified to add a subfolder to avoid filling
  // the root media folder with clutter, refer _cacheExists()
  $mathmultiplugin_urlimg = DOKU_URL.'lib/exe/fetch.php?w=&amp;h=&amp;cache=cache&amp;media=';
  $mathmultiplugin_dirimg = DOKU_INC;

// -----------------------------------------------------------------------

/**
 * All DokuWiki plugins to extend the parser/rendering mechanism
 * need to inherit from this class
 */
class syntax_plugin_mathmulti extends DokuWiki_Syntax_Plugin {

    var $prefix = 'mathmulti_';

    var $inlineopt = array();

    //TODO: take these values from conf/metadata.php
    var $mathmulti_options = array(
        'format' => array('img', 'mml'),
        'idiom' => array('tex', 'plain'),
        'size' => array('9','10','11','12','13','14','15','16','17','18')
        );

    var $mathmulti_engines = array(
        'mimetex'   => array('idiom' => 'tex',
                            'format' => 'img'),
        'itex2mml'  => array('idiom' => 'tex',
                            'format' => 'mml'),
        'plain2mml' => array('idiom' => 'plain',
                            'format' => 'mml')
        );

    var $msg_disable = 'disable';

    var $enable = false;
    var $msg_sent = false;

    /**
     * Initialization?
     */
    function syntax_plugin_mathmulti() {
       $this->msg_disable = $this->getLang($this->prefix.'disable');
       $this->enable = $this->_requirements_ok();
    }

    /**
     * return some info
     */
    function getInfo(){

      return array(
        'author' => 'Stephane Chamberland',
        'email'  => 'stephane.chamberland@ec.gc.ca',
        'date'   => '2006-05-23',
        'name'   => 'MathMulti Plugin'.(!$this->enable ? ' ('.$this->getLang($this->prefix.'disable').')' : ''),
        'desc'   => $this->getLang($this->prefix.'info').
                    (!$this->enable ? "\n(".$this->getLang($this->prefix.'disable').")" : ''),
        'url'    => 'http://wiki.splitbrain.org/plugin:math',
      );
    }

    function getType(){ return 'protected'; }
    function getPType(){ return 'normal'; }
    function getSort(){ return 209; }

    /**
     * Connect pattern to lexer
     */
    function connectTo($mode) {
      $this->Lexer->addEntryPattern('<math(?=[^\r\n]*?>.*?</math>)',$mode,'plugin_mathmulti');
    }

    function postConnect() {
      $this->Lexer->addExitPattern('</math>','plugin_mathmulti');
    }

    /**
     * Handle the match
     */
    function handle($match, $state, $pos, &$handler){

      if ( $state == DOKU_LEXER_UNMATCHED ) {
        list($optstring, $contentstring) = preg_split('/>/u', $match, 2);   // will split into format & math formulae

        //TODO: ?needed? string2lower($optstring);

        $tokens = preg_split('/\s+/', $optstring, 9);  // limit is defensive
        $optionlist = $this->_parseoptions($this->mathmulti_options,
                                          $this->prefix,$tokens);

        $engine = $this->_getengine(
                $optionlist,
                $this->mathmulti_engines,
                $this->prefix,
                !$this->getConf($this->prefix.'iscgi'));

        $align = $this->_checkalign($contentstring);

        return (array($engine, $optionlist['size'], $align, trim($contentstring)));
      }
      return false;
    }

    /**
     * Create output
     */
    function render($mode, &$renderer, $data) {

        if (!$data) return;   // skip rendering for the enter and exit patterns
        list($engine, $size, $align, $eqn) = $data;

        if($mode == 'xhtml'){
            $eqn_html = $renderer->_xmlEntities($eqn);
            if ($this->enable) {
                if ($engine == 'mimetex') {
                    if ($this->getConf($this->prefix.'iscgi')) {
                        $eqn_html =
                            $this->_render_mimetexcgi($eqn,$eqn_html,$size, $align);
                    } else {
                        $eqn_html =
                            $this->_render_mimetex($eqn,$eqn_html,$size, $align);
                    }
                } else if ($engine == 'itex2mml') {
                    $eqn_html =
                            $this->_render_itex2mml($eqn,$eqn_html,$size, $align);
                } else if ($engine == 'plain2mml') {
                    $eqn_html =
                            $this->_render_plain2mml($eqn,$eqn_html,$size, $align);
                } else {
                    $eqn_html =
                            $this->_render_plaintext($eqn,$eqn_html,$size, $align);
                }
            } else {
                $this->_msg($this->msg_disable, -1);
            }
            $renderer->doc .= $eqn_html;
            // return to previous error reporting level
            error_reporting($error_level);
            return true;
        }
        return false;
    }


    function _render_plaintext($eqn,$eqn_html,$size,$align) {
        if ($align == 'normal') {
            return '<span class="math">'.$eqn_html.'</span>';
        } else {
            return '<div class="math '.$align.'">'.$eqn_html.'</div>';
        }
    }

    //TODO: use $size param
    function _render_mimetexcgi($eqn,$eqn_html,$size,$align) {
        $myclass=' class="math"';
        if ($align != 'normal') {
            $math_html = ' class="math media'.$align.'"';
        }
        return '<img src="'.$this->getConf($this->prefix.'mimetex').'?'.
            $eqn_html.'" alt="'.$eqn_html.'" '.$myclass.'/>';
    }

    //TODO: create mathML eqn with mimetex
    function _render_mimetex($eqn,$eqn_html,$size,$align) {
        return $this->_render_plaintext($eqn,$eqn_html,$size, $align);
    }

    //TODO: create and cache img with mimetex
//     function _old_mathmultiimage() {
//         //return $renderer->_xmlEntities($mathmulti_eqn);
//
//         $fontsize0_7 = max(0,min(intval($msize-9,7)));
//
//         $hash = md5(serialize($data.strval($msize)));
//
//         //write eqn in temp file
//         $tmpeqnfile= $mathmultiplugin_urlimg."";
//         $fh = fopen($tmpeqnfile, 'w');
//         fwrite($fh, $mathmulti_eqn);
//         fclose($fh);
//
//         //TODO: remove old img.gif file if any
//         $oldgiffile = "";
//         if (is_file($oldgiffile)) {unlink($oldgiffile);}
//         //TODO: define img.gif file name
//         $giffile = "";
//         //Create img file
//         $fh = popen($this->getConf('mathmulti_mimetex').' -e '.$giffile.' -s '.$fontsize0_7." -f ".$mathmulti_eqn_file,'r');
//         pclose($fh);
//
//         unlink($tmpeqnfile); //TODO: remove eqn temp file
//
//         //Write display image HTML code
//         $myclass=' class="math"';
//         if ($align != 'normal') {
//             $math_html = 'class="math media'.$align.'"';
//         }
//         return '<img src="'.$giffile.'"
//             alt="'.$renderer->_xmlEntities($mathmulti_eqn).
//             $myclass.'/>';
//     }

    //TODO: create mathML eqn with itex2mml
    function _render_itex2mml($eqn,$eqn_html,$size, $align) {
        return $this->_render_plaintext($eqn,$eqn_html,$size, $align);
    }

   //TODO: create mathML eqn with plain2mml
    function _render_plain2mml($eqn,$eqn_html,$size, $align) {
         return $this->_render_plaintext($eqn,$eqn_html,$size, $align);
    }


    /**
     * Check string alignment (return left,right,center,normal)
     * param: Not trimed string
     */
    function _checkalign($mystring){
        $align = 'normal';
        if (strlen($mystring) > 1) {
          $c_first = $mystring{0};
          $c_last = $mystring{strlen($mystring)-1};

          $align = ($c_first == ' ') ? ($c_last == ' ' ? 'center' : 'right') : ($c_last == ' ' ? 'left' : 'normal');
        }
        return $align;
    }

    /**
     * Init options (from default config)
     * Params:
     *    $optionlistdict = array(
     *                       'option_1' => array('val_1', 'val_2'),
     *                        ...);
     *    $confprefix: [string] Prefix of options name in global $conf
     * Return:
     *    $optionlist = array(
     *                       'option_1' => 'val_opt_1',
     *                        ...);
     */
    function _initoptions($optionlistdict,$confprefix) {
        $optionlist = array();
        foreach ($optionlistdict as $option => $optlist) {
            $optionlist[$option] = $this->getConf($confprefix.$option);
        }
        return $optionlist;
    }

    /**
     * Init options (from default config + inline options)
     * Params:
     *    $optionlistdict = array(
     *                       'option_1' => array('val_1', 'val_2'),
     *                        ...);
     *    $confprefix: [string] Prefix of options name in global $conf
     *    $tokenlist: [string array] List of option keywords
     * Return:
     *    $optionlist = array(
     *                       'option_1' => 'val_opt_1',
     *                        ...);
     */
    //TODO: gengeralize to take args as in config plugin
    function _parseoptions($optionlistdict,$confprefix,$tokenlist) {
        $optionlist = $this->_initoptions($optionlistdict,$confprefix);
        foreach ($tokenlist as $token) {
            foreach ($optionlistdict as $option => $optlist) {
                foreach ($optlist as $optionval) {
                    if (trim($token) == trim($optionval)) {
                        $optionlist[$option] = $optionval;
                    }
                }
            }
        }
        return $optionlist;
    }

    /**
     * Look into options and "engine" spec (matching options) to
     *
     * Params:
     *    $optionlist: as obtained by $this->_parseoptions()
     *    $enginedictlist: list of engines and matching option => values
     *        = array('engine_1' => array('option_1' => 'val_1',
     *                                    'option_2' => 'val_2',...),
     *                ...);
     *    $confprefix: [string] Prefix of options name in global $conf
     *    $checkfile: [logical] Check if specified file for engine exist
     * Return:
     *    $engine: [string] engine name (use to render code)
     *             that matches default/specified  options
     *    false: if no "engine" match the options
     */
    function _getengine($optionlist,$enginedictlist,$confprefix,$checkfile) {
        foreach ($enginedictlist as $engine => $engoptdict) {
            $engineok = true;
            foreach ($engoptdict as $option => $optionval) {
                if ($optionlist[$option] != $optionval) {
                    $engineok = false;
                }
            }
            if ($engineok) {
                if ($checkfile) {
                    if (!is_file($this->getConf($confprefix.$engine))) {
                        return false;
                    }
                }
                return $engine;
            }
        }
        return false;
    }

    /**
     * Cheque requirements...
     */
    function _requirements_ok() {

        //check if at least one engine is specified
        $engineok = false;
        foreach ($this->mathmulti_engines as $engine => $engoptdict) {
            if ($this->getConf($this->prefix.$engine) != "") {
                $engineok = true;
            }
        }
        if (!$engineok) {
            $this->msg_disable .= $this->getLang($this->prefix.'noengine');
            return false;
        }

        //check if the default settings/engine are ok
        if (!$this->_getengine(
                $this->_initoptions($this->mathmulti_options,$this->prefix),
                $this->mathmulti_engines,
                $this->prefix,
                !$this->getConf($this->prefix.'iscgi'))) {
          $this->msg_disable .= $this->getLang($this->prefix.'errdefault');
          return false;
        }

        return true;
    }


    /**
     *
     */
    function _cacheExists() {
        global $dirimg, $mathmultiplugin_urlimg, $conf;

        // check for default setting
        if (!isset($dirimg) || !$dirimg) { $dirimg = $this->conf['mediadir']; }
        if ($dirimg == $conf['mediadir']) {
            // we don't want to clutter the root media dir, so create our own subfolder
            $dirimg .= "/cache_mathmultiplugin";
            $mathmultiplugin_urlimg .= "cache_mathmultiplugin%3a";

            if (!@is_dir($dirimg)) {
                $this->_mkdir($dirimg);
            }
        }

        return @is_writable($dirimg);
    }

    /**
     * used to avoid multiple messages
     */
    function _msg($str, $lvl=0) {
        if ($this->msg_sent) return;

        msg($str, $lvl);
        $this->msg_sent = true;
    }

    /**
     *
     */
    // would like to see this function in io.php :)
    function _mkdir($d) {
        global $conf;

        umask($conf['dmask']);
        $ok = io_mkdir_p($d);
        umask($conf['umask']);
        return $ok;
    }

}

//Setup VIM: ex: et ts=4 enc=utf-8 :
